namespace MoreLinq.Reactive;

using System;
using System.Collections.Generic;
using Delegate = Delegating.Delegate;

sealed class Subject<T> : IObservable<T>, IObserver<T>
{
    List<IObserver<T>>? observers;
    bool completed;
    Exception? error;

    bool HasObservers => (this.observers?.Count ?? 0) > 0;
    List<IObserver<T>> Observers => this.observers ??= [];

    bool IsMuted => this.completed || this.error != null;

    public IDisposable Subscribe(IObserver<T> observer)
    {
            if (observer == null) throw new ArgumentNullException(nameof(observer));

            if (this.error != null)
            {
                observer.OnError(this.error);
                return Disposable.Nop;
            }

            if (this.completed)
            {
                observer.OnCompleted();
                return Disposable.Nop;
            }

            Observers.Add(observer);

            return Delegate.Disposable(() =>
            {
                var observers = Observers;

                // Could do the following to find the index of the
                // the observer:
                //
                // var i = observers.FindIndex(o => o == observer);
                //
                // but it would require a closure allocation.

                for (var i = 0; i < observers.Count; i++)
                {
                    if (observers[i] == observer)
                    {
                        if (this.shouldDeleteObserver)
                            observers[i] = null!;
                        else
                            observers.RemoveAt(i);
                        break;
                    }
                }
            });
        }

    bool shouldDeleteObserver; // delete (null) or remove an observer?

    public void OnNext(T value)
    {
            if (!HasObservers)
                return;

            var observers = Observers;

            // Set a flag around iteration to indicate that an observer that
            // disposes their subscription should be marked for deletion
            // instead of being removed from the list of observers. The actual
            // removal is then deferred until after the iteration is complete.

            this.shouldDeleteObserver = true;

            try
            {
                // DO NOT change the following loop into the for-each variant
                // because an observer might dispose its subscription during
                // the call to "OnNext" and List<T>'s enumerator will throw
                // seeing that as a modification of the collection during
                // enumeration.

                for (var i = 0; i < observers.Count; i++)
                    observers[i].OnNext(value);
            }
            finally
            {
                this.shouldDeleteObserver = false;

                // Remove any observers that were marked for deletion during
                // iteration.

                _ = observers.RemoveAll(o => o == null);
            }
        }

    public void OnError(Exception error) =>
        OnFinality(ref this.error, error, (observer, err) => observer.OnError(err));

    public void OnCompleted() =>
        OnFinality(ref this.completed, true, (observer, _) => observer.OnCompleted());

    void OnFinality<TState>(ref TState? state, TState value, Action<IObserver<T>, TState> action)
    {
            if (IsMuted)
                return;

            state = value;

            // Once an error occurs, no other method of the subject is expected
            // to be called so release the list of observers of this subject.
            // The list of observers will be garbage once this method returns.

            var observers = this.observers;
            this.observers = null;

            if (observers == null)
                return;

            foreach (var observer in observers)
                action(observer, value);
        }
}
